package com.kun.lagou.seckillservice.service;

import com.kun.lagou.seckillservice.cache.SeckillActivityLocalCache;
import com.kun.lagou.seckillservice.consumer.CreateSeckillOrderConsumer;
import com.kun.lagou.seckillservice.dao.SeckillActivityDao;
import com.kun.lagou.seckillservice.dao.SeckillOrderDao;
import com.kun.lagou.seckillservice.model.SeckillActivity;
import com.kun.lagou.seckillservice.model.SeckillOrder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * 秒杀服务
 * @author Administrator
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillService {
    private final SeckillActivityDao seckillActivityDao;
    private final SeckillOrderDao seckillOrderDao;
    private final RedissonClient redissonClient;
    private final TransactionTemplate transactionTemplate;
    private final RocketMQTemplate rocketMQTemplate;

    /**读多写少，可以把活动信息提前加载到本地缓存*/
    private final SeckillActivityLocalCache seckillActivityLocalCache;
    private volatile boolean isFinished;

    /**
     * 缓存预热，配置活动的时候做
     */
    @PostConstruct
    public void init() throws ExecutionException {
        /*seckillActivityDao.save(SeckillActivity.builder()
                .stock(100)
                .activityName("name")
                .activityDesc("desc")
                .startTime(Instant.now().plus(1, ChronoUnit.HOURS))
                .endTime(Instant.now().plus(2, ChronoUnit.HOURS))
                .build());*/
        SeckillActivity seckillActivity = seckillActivityLocalCache.getData("1");
        redissonClient.getAtomicLong(getStockKey(seckillActivity.getId()))
                .set(seckillActivity.getStock());
    }

    //TODO 定时任务定期把Redis库存同步数据库

    /**
     * 封装活动库存的key
     * @param activityId
     * @return
     */
    private String getStockKey(long activityId) {
        return String.format("activity:%s:stock", activityId);
    }

    /**
     * 封装活动下单用户的key
     * @param activityId
     * @return
     */
    private String getUserSetKey(long activityId) {
        return String.format("activity:%s:user:set", activityId);
    }

    /**
     * 抢购
     * @param activityId
     * @param userId
     * @return
     */
    public boolean buy(long activityId, long userId) {
        if(isFinished) {
            log.info("活动已经结束！");
            return false;
        }

        /**判断活动时间*/
        try {
            SeckillActivity seckillActivity = seckillActivityLocalCache.getData(Long.toString(activityId));
            Instant startTime = seckillActivity.getStartTime();
            Instant endTime = seckillActivity.getEndTime();
            if(Instant.now().isBefore(startTime)) {
                log.info("活动还没开始，请稍后再来！");
                return false;
            }
            if(Instant.now().isAfter(endTime)) {
                log.info("活动已经结束，下次活动再见！");
                return false;
            }
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //扣减库存
        long count = redissonClient.getAtomicLong(getStockKey(activityId)).decrementAndGet();
        if(count >= 0) {
            return transactionTemplate.execute(s -> {
                /**直接扣减数据库的库存，然后Redis的库存预热多一点*/
                //updatet xxx set stock=stock-1 where stock>0;
                //判断当前用户是否下过单
                if(seckillOrderDao.findByUserIdAndActivityId(userId, activityId).isEmpty()) {
                    seckillOrderDao.save(SeckillOrder.builder()
                            .activityId(activityId)
                            .userId(userId)
                            .build());
                    return true;
                } else {
                    //若用户下过单，则恢复扣减的库存
                    redissonClient.getAtomicLong(getStockKey(activityId)).incrementAndGet();
                    return false;
                }
            });
        } else {
            //没有库存，活动结束
            isFinished = true;
            return false;
        }
    }

    /**
     * 抢购——lua脚本
     * @param activityId
     * @param userId
     * @return
     */
    public boolean buyTwo(long activityId, long userId) {
        if(isFinished) {
            log.info("活动已经结束！");
            return false;
        }
        String script = "if redis.call('exists',KEYS[1]) == 1 then\n" +
                "                 local stock = tonumber(redis.call('get', KEYS[1]))\n" +
                "                 if( stock <= 0 ) then\n" +
                "                    return -1\n" +
                "                 end;\n" +
                "                 if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then\n" +
                "                   return -2\n" +
                "                 end;\n" +
                "                 redis.call('SADD', KEYS[2], ARGV[1]);\n" +
                "                 redis.call('decr',KEYS[1]);\n" +
                "                 return stock - 1;\n" +
                "             end;\n" +
                "             return -1;";

        long count = redissonClient.getScript().eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.INTEGER,
                List.of(getStockKey(activityId)), getUserSetKey(activityId), userId);
        if(count >= 0) {
            //TODO 出异常了怎么办？用分布式事务
            log.info("当前库存: {}", count);
            //MQ下单
            rocketMQTemplate.asyncSend(CreateSeckillOrderConsumer.TOPIC,
                    CreateSeckillOrderConsumer.CreateSeckillOrderMessage.builder()
                            .userId(userId)
                            .acitvityId(activityId)
                            .build(), new SendCallback() {
                        @Override
                        public void onSuccess(SendResult sendResult) {
                            log.info("send seckill order msg success:{} {}", userId, activityId);
                        }

                        @Override
                        public void onException(Throwable throwable) {
                            log.info("send seckill order msg error:{} {}", userId, activityId);

                        }
                    });

            return true;
        }
        if(count == -1) {
            isFinished = true;
        }
        return false;
    }
}
